package com.laguille.games.tetris.model;

import java.util.ArrayList;
import java.util.List;

import com.laguille.games.tetris.model.Tetromino.Coordinates;
import com.laguille.games.tetris.model.Tetromino.Direction;
import com.laguille.games.tetris.model.Tetromino.TetrominoBlock;
import com.laguille.games.tetris.model.Tetromino.TetrominoPiece;
import com.laguille.games.tetris.rules.RuleEngine;

public class Field {

	public final static int WIDTH = 10; // number of cells
	public final static int HEIGHT = 20; // number of cells
	
	private final TetrominoBlock[][] matrix;
	
	public Field() 
	{
		matrix = new TetrominoBlock[WIDTH][HEIGHT];
	}
	
	public boolean isOccupied(Coordinates coordinates)
	{
		return 	matrix[coordinates.getX()][coordinates.getY()] != null;
	}
	
	public boolean isOutOfBounds(Coordinates coordinates)
	{
		return 	coordinates.getX() < 0 || coordinates.getX() >= WIDTH
			||	coordinates.getY() < 0 || coordinates.getY() >= HEIGHT;
	}
	
	public List<TetrominoBlock> getAllBlocks()
	{
		List<TetrominoBlock> blocks = new ArrayList<TetrominoBlock>();
		for (int row = 0 ; row < matrix.length ; row++)
		{
			for (int col = 0 ; col < matrix[row].length ; col++) 
			{
				if (matrix[row][col] != null) 
				{
					blocks.add(matrix[row][col]);
				}
			}
		}
		return blocks;
	}
	
	/**
	 * Move the input piece into the specified direction
	 * @param piece the piece to move
	 * @param direction the direction to move the piece
	 * @return true if the move was successful, false otherwise
	 */
	public boolean movePiece(TetrominoPiece piece, Direction direction)
	{
		Coordinates[] newCoordinates = new Coordinates[4];
		Coordinates c;
		int i = 0;
		for (TetrominoBlock b : piece.getBlocks())
		{
			if (direction == Direction.LEFT)
			{
				c = b.getCoordinates().shiftLeft();
			}
			else if (direction == Direction.RIGHT)
			{
				c = b.getCoordinates().shiftRight();
			}
			else if (direction == Direction.DOWN)
			{
				c = b.getCoordinates().shiftDown();
			}
			else
			{
				throw new UnsupportedOperationException("The piece can only be moved right or left");
			}
			newCoordinates[i++] = c;
		}
		if (!RuleEngine.isPositionValid(this, piece, newCoordinates))
		{
			return false;
		}
		// at this point, we have already validated that the move is valid
		// we clean up the old position
		for (TetrominoBlock b : piece.getBlocks())
		{
			matrix[b.getCoordinates().getX()][b.getCoordinates().getY()] = null;
		}
		
		// and update the new one
		for (int j = 0 ; j < newCoordinates.length ; j++)
		{
			Coordinates co = newCoordinates[j];
			TetrominoBlock b = piece.getBlocks()[j];
			b.setCoordinates(co);
			matrix[b.getCoordinates().getX()][b.getCoordinates().getY()] = b;
		}
		return true;
	}
	
	/**
	 * Rotate the input piece into the specified direction
	 * @param piece the piece to move
	 * @param direction the direction to rotate the piece
	 * @return true if the move was successful, false otherwise
	 */
	public boolean rotatePiece(TetrominoPiece piece, Direction direction)
	{
		TetrominoPiece p;
		Coordinates[] newCoordinates = new Coordinates[4];
		int i = 0;
		
		if (direction == Direction.LEFT)
		{
			p = piece.rotateLeft();
		}
		else if (direction == Direction.RIGHT)
		{
			p = piece.rotateRight();
		}
		else
		{
			throw new UnsupportedOperationException("The piece can only be rotated right or left");
		}
		
		for (TetrominoBlock block : p.getBlocks())
		{
			if (!RuleEngine.isPositionValid(this, piece, block.getCoordinates()))
				return false;
			newCoordinates[i++] = block.getCoordinates();
		}
		
		// at this point, we have already validated that the move is valid
		// we clean up the old position
		for (TetrominoBlock b : piece.getBlocks())
		{
			matrix[b.getCoordinates().getX()][b.getCoordinates().getY()] = null;
		}
		
		// and update the new one
		for (int j = 0 ; j < newCoordinates.length ; j++)
		{
			Coordinates co = newCoordinates[j];
			TetrominoBlock b = piece.getBlocks()[j];
			b.setCoordinates(co);
			matrix[b.getCoordinates().getX()][b.getCoordinates().getY()] = b;
		}
		piece.setPosition(p.getPosition());
		return true;
	}
	
	/**
	 * Add the input piece to the field
	 * @param piece the piece to add
	 * @return true if the add was successful, false otherwise
	 */
	public boolean addPiece(TetrominoPiece piece)
	{
		if (!RuleEngine.isPositionValid(this, piece))
			return false;
		
		for (TetrominoBlock block : piece.getBlocks())
		{
			matrix[block.getCoordinates().getX()][block.getCoordinates().getY()] = block;
		}
		
		return true;
	}
	
	public boolean atTheBottom(TetrominoPiece piece)
	{
		Coordinates newC;
		for (TetrominoBlock b : piece.getBlocks())
		{
			int x = b.getCoordinates().getX();
			int y = b.getCoordinates().getY();
			newC = new Coordinates(x, y - 1);
			if (isOccupied(newC) || isOutOfBounds(newC))
				return true;
		}
		return false;
	}
	
	/**
	 * Return a list of integer that indicate the index of one one more full rows
	 * @return the index of the full rows, empty if no full row
	 */
	public List<Integer> getFullLines() {
		List<Integer> indexes = new ArrayList<Integer>(4);
		for (int row = 0 ; row < HEIGHT ; row++)
		{
			boolean isFull = true;
			for (int col = 0 ; col < WIDTH ; col++)
			{
				isFull = (matrix[col][row] != null);
				if (!isFull)
					break;
			}
			if (isFull)
			{
				indexes.add(row);
				System.out.println("Found full line at row " + row);
			}
		}
		return indexes;
	}

	/**
	 * Remove the lines which index is passed as an input
	 * @param lineNumbers the lines to remove from the matrix
	 */
	public void removeLines(Integer...lineNumbers) {
		for (Integer row : lineNumbers)
		{
			System.out.println("Removing row " + row);
			for (int col = 0 ; col < WIDTH ; col++)
			{
				if (matrix[col][row] != null)
				{
					matrix[col][row] = null;
				}
			}
		}
	}
	
	/**
	 * Shifts down by n the lines above the input row index 
	 * @param rowIndex the lines above this index needs to be moved n times down
	 * @param n the number of times to move the line down
	 */
	public void shiftLinesDown(int rowIndex, int n) {
		for (int row = rowIndex + 1; row < HEIGHT ; row++)
		{
			if (isRowEmpty(row))
				break;
			System.out.println("Shifting line down from " + row + " to row " + (row - n));
			for (int col = 0 ; col < WIDTH ; col++)
			{
				if (matrix[col][row] != null)
				{
					matrix[col][row - n] = matrix[col][row];
					matrix[col][row - n].setCoordinates(new Coordinates(col, row - n));
				}
			}
			removeLines(row);
		}
	}
	
	private boolean isRowEmpty(int row)
	{
		for (int col = 0 ; col < WIDTH ; col++)
		{
			if (matrix[col][row] != null)
				return false;
		}
		return true;
	}
	
	@Override
	public String toString() {
		StringBuffer sb = new StringBuffer();
		for (int row = HEIGHT - 1 ; row >= 0 ; row--)
		{
			for (int col = 0 ; col < WIDTH ; col++)
			{
				if (matrix[col][row] == null)
					sb.append("o");
				else
					sb.append("x");
			}
			sb.append("\n");
		}
		return sb.toString();
	}
	
}
